<?

/** Class to handle row lockings.
 *
 *  This collection of functions (libary) is licensed under GPL2.
 *  See LICENSE or www.gnu.org for more details.
 *  @author Jyry Kuukkanen
 *  @copyright Jyry Kuukkanen
 */

include_once("sosqllo.php");
include_once("sosqlhi.php");

/** lock table related constants */
define(SOLOCKTABLE, "solocks");
define(SOLCOL_OWNER, SOLOCKTABLE.".owner");
define(SOLCOL_SESSION, SOLOCKTABLE.".session");
define(SOLCOL_LOCKTIME, SOLOCKTABLE.".locktime");
define(SOLCOL_TABLENAME, SOLOCKTABLE.".tablename");
define(SOLCOL_TABLEKEY, SOLOCKTABLE.".tablekey");

/** locking methods */
define(SOLM_NOP, 0);        /** no operation - for internal use only */
define(SOLM_NORMAL, 1);     /** normal locking */
define(SOLM_OVERRIDE, 2);   /** overriding other lock (superuser) */
define(SOLM_UNLOCK, 3);     /** unlocking */

/** Lock statuses */
define(SOLS_FAILED, 1);     /** un/locking failed */
define(SOLS_CONNERROR, 2);  /** error connection db */
define(SOLS_OWN, 3);        /** locked for this owner & session */
define(SOLS_REOWN, 4);      /** already locked by the same owner & session */
define(SOLS_NONE, 5);       /** not even a bit locked */
define(SOLS_ELSE, 6);       /** locked by some other owner */
define(SOLS_LOCAL, 7);      /** locked by same owner in diff session */
define(SOLS_PARAMERROR, 8); /** programmer is silly */


/** Locking properties class. This is used by solock class.
 *  @var string $TableName Name of the table of the row to loc
 *  @var array/string $Key Row key. Each element represend a key col
 *  @var timestamp $LockTime Time stamp when the lock was acquired.
 *  @var string $Owner Owner of the lock: username
 *  @var string $Session Session of the lock: session id
 *  @var SOLS_* $Status Status of the lock (ro)
 *  @package solock
 */
class soLockProps {
    var $TableName;
    var $Key;
    var $LockTime;
    var $Owner;
    var $Session;
    var $Status;


    /** Constructor
     *  @package solock
     */
    function soLockProps() {
        $this->Key = array();
    } // soLockPros


    /** Returns structure as string.
     *  Very handy for debuggin purposes.
     *  @param string $PropertyDelim Property delimiter str.
     *  @package solock
     */
    function dump($PropertyDelim = "\n") {
        return "TableName=$this->TableName".$PropertyDelim.
               "Key=".implode("*", $this->Key).$PropertyDelim.
               "LockTime=$this->LockTime".$PropertyDelim.
               "Owner=$this->Owner".$PropertyDelim.
               "Session=$this->Session".$PropertyDelim.
               "Status=$this->Status".$PropertyDelim;
    } // dump

}; // soLockProps


/** Class to handle row lockins.
 *  This class handles locking issues.
 *  Example: Usage:
 *  $lock = new soLock("dbaccess_str", "tablename", "keyvalue", "ownerid", "sessionid");
 *  $result = $lock->execute(SOLM_NORMAL);
 *  Example: = SOLS_OWN) $result = $lock->execute(SOLM_OVERRIDE)
 *  Example: = SOLS_OWN) {
 *      echo $lock->getStatus(1)."\n"; // 1=get status as text, 0=as SOLS_*
 *  } else {
 *      $result = $lock->execute(SOLM_UNLOCK);
 *  };
 *  @var soLockProps $Params Lock status to set
 *  @var soLockProps $LockInfo Status of last lock queried
 *  @var soDa $Soda Database access object.
 *  @var int $Retries Lock retry count
 *  @var int $RetryDelay Delay in ms to wait between retries
 *  @package solock
 */
class soLock {

    var $Params;
    var $LockInfo;

    /** @access private */
    var $Soda;
    var $Retries = 5;
    var $RetryDelay = 100;

    /** constructor
     *  Sets parameters and tries to execute locking method.
     *  @param string $DbAccessStr Database access string
     *  @param string $TableName Name of the table to set/unset a lock for.
     *  @param array/any $Key Key values for the table.
     *  @param string $Owner Owner name of the lock.
     *  @param string $Session Session name for the owner of the lock.
     *  @param SOLM_* $LockMethod Locking method (lock/unlock, etc. - see solock for details)
     *  @package solock
     */
    function soLock($DbAccessStr = "", $TableName = "", $Key = array(),
                    $Owner = "", $Session = "", $LockMethod = SOLM_NOP) {
        $this->Soda = new soDa();
        if ($DbAccessStr != "") $this->setDbAccess($DbAccessStr);
        $this->Soda->setTable(SOLOCKTABLE);

        $this->Params = new soLockProps();
        $this->Params->TableName = $TableName;
        $this->Params->Key = $Key;
        $this->Params->Owner = $Owner;
        $this->Params->Session = $Session;

        $this->LockInfo = new soLockProps();
        $this->LockInfo->TableName = "";
        $this->LockInfo->Key = "";
        $this->LockInfo->Owner = "";
        $this->LockInfo->Session = "";

        if ($LockMethod != SOLM_NOP) $this->execute($LockMethod);
    } // constructor


    /** Sets an db connect string in place.
     *  @param string $DbAccessStr Db connection string
     *  @package solock
     */
    function setDbAccess($DbAccessStr) {
        $this->Soda->setDbAccess($DbAccessStr);
    } //


    /** Returns current lock status for table+row
     *  Gets locking status of current row information in
     *  @param bool $AsText When set(1), returns status as text
     *  @return int/string Lock status no. or text
     *  @package solock
     */
    function getStatus($AsText = 0) {
        $this->Params->Status = $this->queryLock($this->Params->TableName,
                                                 $this->Params->Key,
                                                 $this->Params->Owner,
                                                 $this->Params->Session);
        if ($AsText) {
            return $this->getStatusText($this->Params->Status);
        } else {
            return $this->Params->Status;
        };
    } // getStatus


    /** Returns lock status information in words.
     *  @param string $Status Status no. to get a text for
     *  @return string Status as text
     *  @package solock
     */
    function getStatusText($Status) {
        switch ($Status) {
            case SOLS_FAILED:
                $result = "un/locking failed";
                break;
            case SOLS_CONNERROR:
                $result = "error connection db";
                break;
            case SOLS_OWN:
                $result = "locked for this owner & session";
                break;
            case SOLS_REOWN:
                $result = "locked already for this owner & session";
                break;
            case SOLS_NONE:
                $result = "not locked";
                break;
            case SOLS_ELSE:
                $result = "locked by some other owner";
                break;
            case SOLS_LOCAL:
                $result = "locked by same owner in diff session";
                break;
            default:
                $result = "programmer is silly"; /* SOLS_PARAMERROR */
        }; // switch

        return $result;
    } // getStatusText


    /** Sets parameters for the lock
     *  Sets parameters for locking a row in a table.
     *  @param string $TableName Name of the table to set/unset a lock for.
     *  @param array/any $Key Key values for the table.
     *  @param string $Owner Owner name of the lock.
     *  @param string $Session Session name for the owner of the lock.
     *  @return bool Returns true(1) if $Key is an array, otherwise false (0).
     *  @package solock
     */
    function setParams($TableName, $Key, $Owner, $Session) {
        if (!is_array($Key)) return 0;

        $this->Params->TableName = $TableName;
        $this->Params->Key = $Key;
        $this->Params->Owner = $Owner;
        $this->Params->Session = $Session;
        /* status is not used with params, only with info */

        soDebug("soLock->setParams($TableName, ".implode(":", $Key).
                                   ", $Owner, $Session)", SOD_DET);

        return 1;
    } // setParams


    /** Performs queryLock with setLockParams values.
     *  Calls queryLock with values stored using setLockParams
     *  @return SOLS_* Returns the lock status. Entire lock information is
     *      returned through LockInfo property.
     *  @package solock
     */
    function readStatus() {
        return $this->queryLock($this->Params->TableName,
                                $this->Params->Key,
                                $this->Params->Owner,
                                $this->Params->Session);
    } // readStatus


    /** Queries a lock status.
     *  Fetches locking information of a table and a row. Owner and session
     *  may be passed to define whether a lock is held by owner itself or
     *  someone else.
     *  @param string $TableName Table name
     *  @param array/string $Key Key to row in a table.
     *  @param string $Owner Owner name.
     *  @param string $Session SessionID
     *  @return SOLS_* Returns the lock status. Entire lock information is
     *      returned through LockInfo property.
     *  @package solock
     */
    function queryLock($TableName, $Key, $Owner, $Session, $AutoCreate = 1) {
        if (!is_array($Key)) return SOLS_PARAMERROR;

        $keystr = soUrlEncodeArray($Key);
        soDebug("soLock->queryLock(Table=$TableName, Key=$keystr, ".
                "Owner=$Owner, Session=$Session)", SOD_LO);
        $this->Soda->setAction(SODA_SELECT);
        $this->Soda->setSection(SODA_DATA, SOLCOL_OWNER." ".SOLCOL_SESSION.
                                " ".SOLCOL_LOCKTIME,
                                array(), SODA_RESET);
        $this->Soda->setSection(SODA_WHERE, SOLCOL_TABLENAME." ".
                                SOLCOL_TABLEKEY,
                                array($TableName, $keystr), SODA_RESET);
        $result = $this->Soda->run();

        if ($result) {
            /* some kind of lock exists */

            if ($this->Soda->Results->count()) {
                $this->LockInfo->TableName = $TableName;
                $this->LockInfo->Key = $Key;
                $this->LockInfo->Owner =
                    $this->Soda->Results->getValue(SOLCOL_OWNER, 0);
                $this->LockInfo->Session =
                    $this->Soda->Results->getValue(SOLCOL_SESSION, 0);
                $this->LockInfo->LockTime =
                    $this->Soda->Results->getValue(SOLCOL_LOCKTIME, 0);
                if ($this->LockInfo->Owner != $Owner) {
                    /* locked somewhere else */
                    $this->LockInfo->Status = SOLS_ELSE;

                } else { // v- same owner

                    /* locked by same owner in dirrerent session */
                    if ($this->LockInfo->Session != $Session) {
                        $this->LockInfo->Status = SOLS_LOCAL;

                    } else { // v- same owner & session
                        $this->LockInfo->Status = SOLS_OWN;
                    };
                }; // else if diff owner

            } else { // lock info

                /* no lock exists */
                $this->LockInfo->Status = SOLS_NONE;
                $this->LockInfo->TableName = $TableName;
                $this->LockInfo->Key = $Key;
                $this->LockInfo->Owner = $Owner;
                $this->LockInfo->Session = $Session;
                $this->LockInfo->LockTime = "";
            }; // else if lock info

        } else {
            soDebug("soLock->queryLock: error querying lock", SOD_LO);
            if ($AutoCreate) {
                if ($this->createLockTable()) {
                    $this->LockInfo->Status = $this->queryLock($TableName,
                                                               $Key,
                                                               $Owner,
                                                               $Session, 0);
                } else {
                    $this->LockInfoStatus = SOLS_CONNERROR;
                }; // create ok
            } else {
                /* connection error */
                $this->LockInfo->Status = SOLS_CONNERROR;
            };
        }; // else if $result

        $result = $this->LockInfo->Status;
        return $result;
    } // queryLock


    /** Tries to a lock or unlock a row.
     *  Tries to lock or unlock current row. Lock override is also
     *  possible. See SOLM_* for locking methods.
     *  @param SOLM_* $LockMethod Locking method to use.
     *  @return SOLS_* Returns lock status after execution. Unlocking should
     *      return SOLS_NONE and locking (normal or override) sould return
     *      SOLS_OWN. Any other indicates an error.
     *  @package solock
     */
    function execute($LockMethod) {
        $t = 0;
        do {
            if ($t) {
                soDebug("soLock->execute: retry $t/$Lockmethod", 
                        SOD_LO);
            }; // if $t

            $t++;
            switch ($LockMethod) {
                case SOLM_NORMAL:
                case SOLM_OVERRIDE:
                    $result = $this->__addLock($LockMethod);
                    /* if lock failed and retry count not reached,
                       then wait for a while and go on */
                    if (($result != SOLS_OWN) &&
                        ($t < $this->Retries)) {
                        usleep($this->RetryDelay);
                    }; // if ($result)
                    break;

                case SOLM_UNLOCK:
                    $result = $this->__delLock();
                    $t = $this->Retries;
                    break;

                default:
                    /* $LockMethod is somehing silly */
                    $result = SOLS_PARAMERROR;
            }; // switch

        } while (($result != SOLS_OWN) && ($t < $this->Retries));

        $this->LockInfo->Status = $result;
        return $result;
    } // execute


    /** Delete session locks.
     *  Deletes all locks that belongs to a given session
     *  @param string $SessionId The session whose locks to remove
     *  @return bool True(1) on success, otherwise false(0)
     *  @package solock
     */
    function delSesLocks($SessionId) {
        $this->Soda->setAction(SODA_DELETE);
        $this->Soda->setSection(SODA_WHERE, SOLCOL_SESSION,
                                array($SessionId),
                                SODA_RESET);
        $result = $this->Soda->run();

        return $result;
    } // delSesLocks


    /** Delete a lock.
     *  Called by execute.
     *  @param int $Override When set(1), deletes the lock whether it's not
     *      owned by current owner defined in parameters.
     *  @package solock
     */
    function __delLock($Override = 0) {

        $status = $this->getStatus();
        if (($status == SOLS_OWN) || ($Override)) {
            /* own lock, try deleting it */

            $keystr = soUrlEncodeArray($this->Params->Key);
            $this->Soda->setAction(SODA_DELETE);
            $this->Soda->setSection(SODA_WHERE, SOLCOL_TABLENAME." ".
                                    SOLCOL_TABLEKEY,
                                   array($this->Params->TableName, $keystr),
                                   SODA_RESET);
            $result = $this->Soda->run();

            /* was sql exec ok? */
            if ($result) {
                $result = SOLS_NONE;
            } else { // if !$result
                $result = SOLS_FAILED;
            };
        } else {

            /* SOLS_NONE returned if ok, otherwise current lock status
               or SOLS_FAILED if sql exec error */
            $result = $status;
        };

        return $result;
    } // __delLock


    /** Adds a new lock for a row.
     *  @param SOLM_* $LockMethod Locking method to use.
     *  @return SOLS_* Returns SOLS_OWN on success, otherwise the current lock status.
     *  @package solock
     */
    function __addLock($LockMethod) {

        /* Override locking: delete any rowlock and then add using
           normal locking. */

        soDebug("soLock->addLock($LockMethod) table=".$this->Params->TableName.
                ", key(s)=".implode(":", $this->Params->Key), SOD_LO);
        $this->Params->LockTime = date("YmdHis", time());
        if ($LockMethod == SOLM_NORMAL) {
            $keystr = soUrlEncodeArray($this->Params->Key);
            $this->Soda->setAction(SODA_INSERT);
            $this->Soda->setSection(SODA_DATA,
                                    SOLCOL_TABLENAME." ".SOLCOL_TABLEKEY." ".
                                    SOLCOL_OWNER." ".SOLCOL_SESSION." ".
                                    SOLCOL_LOCKTIME,
                                    array($this->Params->TableName,
                                          $keystr, $this->Params->Owner,
                                          $this->Params->Session,
                                          $this->Params->LockTime), SODA_RESET);

            $result = $this->Soda->run();
            if ($result) {
                $result = SOLS_OWN;
            } else {
                $result = $this->getStatus();
                /* If table was missing, status == SOLS_NONE, so an new
                 * attempt should now be done to try locking. */
                if ($result == SOLS_NONE) {
                    $result = $this->__addLock($LockMethod);
                } else {
                    if ($result == SOLS_OWN) $result = SOLS_REOWN;
                };
            };
        } else {

            /* delLock(1) = override any other's lock */
            if ($this->__delLock(1) == SOLS_NONE) {
                $result = $this->__addLock(SOLM_NORMAL);
            } else {
                $result = SOLS_FAILED;
            };
        };

        return $result;
    } // __addlLock


    /** Creates the lock table.
     *  Used by queryLock if tables seems to be missing.
     *  @return bool Returns true(1) on success or false(0) if create fails.
     *  @package solock
     */
    function createLockTable() {
        $this->Soda->setAction(SODA_CREATE);
        $this->Soda->setSection(SODA_DATA, "tablename tablekey owner ".
                                "session locktime *",
                                array("char(30) not null",
                                      "char(120) not null",
                                      "char(40) not null",
                                      "char(20)",
                                      "char(14)",
                                      "tablename, tablekey"),
                                      SODA_RESET);
        return $this->Soda->run();
    } // createLockTable


}; // soLock

?>